This is an archived snapshot of W3C's public bugzilla bug tracker, decommissioned in April 2019. Please see the home page for more details.

Bug 10624 - [Selection] Selection anchorNode/anchorOffset/focusNode/focusOffset do not match existing browser behaviour
Summary: [Selection] Selection anchorNode/anchorOffset/focusNode/focusOffset do not ma...
Status: RESOLVED FIXED
Alias: None
Product: WebAppsWG
Classification: Unclassified
Component: HISTORICAL - HTML Editing APIs (show other bugs)
Version: unspecified
Hardware: All All
: P2 major
Target Milestone: ---
Assignee: Aryeh Gregor
QA Contact: HTML Editing APIs spec bugbot
URL:
Whiteboard:
Keywords:
Depends on: 10798
Blocks:
  Show dependency treegraph
 
Reported: 2010-09-13 11:22 UTC by Tim Down
Modified: 2011-12-21 09:10 UTC (History)
10 users (show)

See Also:


Attachments
Proposed spec patch (6.42 KB, patch)
2011-01-12 01:20 UTC, Aryeh Gregor
Details

Description Tim Down 2010-09-13 11:22:37 UTC
The anchorNode, anchorOffset, focusNode and focusOffset properties of a Selection object lack the subtlety of current browser implementations. Specifically, they make no provision for a selection to be "backwards"; that is, for a selection to have been made by starting at a point in the document and stopping at a point earlier in the document. In Mozilla, WebKit and Opera the point at which the user started creating the selection is reflected in anchorNode and anchorOffset and the finish point of the selection in focusNode and focusOffset, which crucially may be earlier in the document than anchorNode and anchorOffset. This feature is lost in HTML 5's Range-obsessed Selection specification, which guarantees that focusNode and focusOffset cannot be a point earlier in the document than that specified by anchorNode and anchorOffset. This is a serious loss of the richness of current browser implementations. The relevant section of the spec that needs to be revised is the following:

"The anchorNode  attribute must return the value returned by the startContainer attribute of the last Range object in the list, or null if the list is empty.

The anchorOffset attribute must return the value returned by the startOffset attribute of the last Range object in the list, or 0 if the list is empty.

The focusNode attribute must return the value returned by the endContainer attribute of the last Range object in the list, or null if the list is empty.

The focusOffset attribute must return the value returned by the endOffset attribute of the last Range object in the list, or 0 if the list is empty."

I would suggest that you can still specify these properties in terms of the final Range within the selection but make provision for backwards selections. Something like the following for anchorNode, coupled with a definition of the direction of the selection:

"The anchorNode attribute must return the value returned by either the startContainer or endContainer attribute of the last Range object in the list depending on the direction of the selection, or null if the list is empty."
Comment 1 Aryeh Gregor 2011-01-10 20:48:10 UTC
Behavior here is extremely inconsistent.  Opera 11, Chrome dev, and Firefox 4.0b8 all do provide backwards anchor/focus points if the user selects backwards.  IE9 beta exactly matches the Selection anchor/focus points to its first Range, meaning they're always forwards.  (Not the last Range -- Firefox does the last Range like the spec, everyone else does first.)

Where it gets weird is when you try modifying the Range when the Selection is backwards.  In IE9 it acts pretty logically.  Chrome and Opera seem to simply not reflect changes made to the Range in the Selection's anchorOffset/focusOffset.  IE9 works as you'd expect from the spec.  Firefox uses some unclear logic to decide whether to keep the Selection reversed or not.  Test page:

<!DOCTYPE html>
<p>Click after the "c" and drag backward to the "b" so that both letters are
highlighted, then click the button.
<p id=target>Abcd
<p><button onclick=output()>Click</button>
<div id=out></div>
<script>
function output() {
var selection = window.getSelection();
var range = selection.getRangeAt(0);
var div = document.getElementById("out");
var target = document.getElementById("target").firstChild;
div.innerHTML = selection.anchorOffset + ", " + selection.focusOffset + "; ";
range.setStart(target, 2);
range.setEnd(target, 3);
div.innerHTML += selection.anchorOffset + ", " + selection.focusOffset + "; ";
var range = document.createRange();
range.setStart(target, 0);
range.setEnd(target, 1);
selection.addRange(range);
div.innerHTML += selection.anchorOffset + ", " + selection.focusOffset + "; ";
if ("removeRange" in selection) {
    // Skip this for WebKit
    selection.removeRange(selection.getRangeAt(0));
    div.innerHTML += selection.anchorOffset + ", " + selection.focusOffset + "; ";
}
selection.removeAllRanges();
selection.addRange(range);
div.innerHTML += selection.anchorOffset + ", " + selection.focusOffset;
}
</script>

Firefox 4.0b8: 3, 1; 3, 2; 1, 0; 3, 2; 0, 1
Chrome dev:    3, 1; 3, 1; 0, 3;       0, 1
Opera 11:      3, 1; 3, 1; 3, 1; 3, 1; 0, 1
IE9 beta:      1, 3; 2, 3; 2, 3; 0, 0; 0, 1

Notice that when you change the Range's start and end points, Firefox keeps it reversed.  When you append a new Range, it's still reversed.  But when you do removeAllRanges(), that resets it so now it's forwards.  This all seems kind of crazy.  (Notice that this test is kind of confused by the fact that Firefox has anchorOffset/focusOffset key off the last Range, per spec, while the other three key off the first Range, so the third output is inconsistent partly because of that.)

So it looks like the Selection has some type of "backwards" flag associated with it that's maintained across certain operations and cleared by certain operations.  I could try to reverse-engineer this, but it seems like Firefox is the only one that does anything like this at all, so maybe it would be simplest to ask Firefox what they do.
Comment 2 Aryeh Gregor 2011-01-10 20:59:14 UTC
. . . by the way, what are the use-cases for exposing backwards selections like this?  What's wrong with the IE9 behavior in practice?
Comment 3 Tim Down 2011-01-10 22:46:48 UTC
The use case I had in mind is simple: complete restoration of a previous selection state. For example, imagine a browser-based editor with a custom undo stack. Undo should ideally restore the selection so that it's identical to its state prior to the action being undone. This includes the selection's direction, which is important since it affects cursor key behaviour.
Comment 4 Aryeh Gregor 2011-01-11 20:42:39 UTC
How are you restoring the state?  There's no way I see to clone a Selection, and cloning Ranges doesn't work in my testing.  In Firefox, the direction info seems to be part of the Selection object; if you store the Ranges somewhere and later wipe out the Selection's Ranges and replace them, you get the second Selection's direction, not the first.
Comment 5 Tim Down 2011-01-11 21:32:18 UTC
A list of Ranges and a Boolean flag indicating direction is enough to completely recreate a selection. If you don't modify the DOM in between saving and restoring selection state, you can use clones of the selection Ranges (which works fine). Otherwise, you need some application-specific means of storing Range information. None of which is important: the real point is that you can use the extend() method of the selection to create a backwards selection. For example:

function selectRangeBackwards(range) {
    var sel = window.getSelection();
    var endRange = range.cloneRange();
    endRange.collapse(false);
    sel.addRange(endRange);
    sel.extend(range.startContainer, range.startOffset);
}

This is slightly off topic for this bug, since I filed a separate one for adding extend() to the spec (#10691). This one is about being able to tell programmatically whether a selection is backwards, something that the spec does not allow for and redefines anchor* and focus* properties to prevent them being able to tell you.
Comment 6 Tim Down 2011-01-11 21:45:32 UTC
One other point you touched on in your post: when getRangeAt() is called, Firefox is the only one to return the same actual Range object as the one that was supplied to addRange(). WebKit and Opera (haven't tested IE9, I have Windows XP at home) mangle Ranges on the way in and in some situations store ranges with different properties to those that were added, in WebKit's case because there are serious problems with its selection object (for example, you cannot place the caret at the start of a text node). Whether or not getRangeAt() should return the self-same Range that was added via addRange() does not seem to be specified. What should happen when a Range that has been added to a selection is mutated is also not specified. I would say both are very good candidates for being added to the spec, even if it's just to say that the behaviour is up to the implementation.
Comment 7 Aryeh Gregor 2011-01-12 00:47:50 UTC
(In reply to comment #5)
> None of which is important: the real point is that
> you can use the extend() method of the selection to create a backwards
> selection.

Ah, okay.  I've confirmed that this works.  A bit roundabout, but I can see the utility.  I'll write up some spec changes.
Comment 8 Aryeh Gregor 2011-01-12 01:20:57 UTC
Created attachment 941 [details]
Proposed spec patch

This specs a reasonable approximation of Firefox's behavior, based on a combination of black-box testing and source code inspection.  No other browser seems to do anything sane here.  I haven't actually tried running the preprocessor or anything since I have no idea how that works, so I'm more or less cargo-culting the specific markup.  Some stylistic thoughts:

* Is the "(respectively)" wording clear enough, or should it be more long-winded and explicit?
* Should it be "should" or "must" for the direction of the selection if created by the user?
Comment 9 Aryeh Gregor 2011-01-12 01:21:17 UTC
Ms2ger, can you look at this and accept it if appropriate?
Comment 10 Ms2ger 2011-01-12 09:08:33 UTC
LGTM.

https://bitbucket.org/ms2ger/dom-range/changeset/7963331ee0cb

(In reply to comment #8)
> Created attachment 941 [details]
> Proposed spec patch
> 
> This specs a reasonable approximation of Firefox's behavior, based on a
> combination of black-box testing and source code inspection.  No other browser
> seems to do anything sane here.  I haven't actually tried running the
> preprocessor or anything since I have no idea how that works, so I'm more or
> less cargo-culting the specific markup.

It worked out fine; ping me on IRC if you'd like to get it set up.

> Some stylistic thoughts:
> 
> * Is the "(respectively)" wording clear enough, or should it be more
> long-winded and explicit?

That's fine, IMO.

> * Should it be "should" or "must" for the direction of the selection if created
> by the user?

I made it a must, people can complain if that's too harsh.

(Also, note that IE's implementation is spec-based.)
Comment 11 Aryeh Gregor 2011-01-12 19:36:46 UTC
Also committed a test (to the HTML5 test repo for now):

http://dvcs.w3.org/hg/html/raw-file/6243b5d14211/tests/submission/AryehGregor/selection-dir.html
Comment 12 Ian 'Hixie' Hickson 2011-01-12 20:35:50 UTC
Some more things to test for this:

* what happens on either side of bidi situations? e.g. in rtl text, use drags r-to-l, is it "forwards"? user drags l-to-r, is it "backwards"?  How about going from rtl text into ltr text? Vice versa? Forwards and backwards?

* what happens when the user manually extends the selection? e.g. drag a selection, then shift click elsewhere to extend it, both before and after the selection, with both a backwards and a forwards selection.

* what happens when the user selects a range by clicking once then shift-clicking elsewhere, as opposed to dragging?

* what happens when DOM nodes that contain the selection move? (e.g. if the selection starts in a span and ends outside it, and that span gets moved to after the end of the selection, as well as vice-versa) what happens if the direction of the text changes? (e.g. you have a backwards selection in a <bdo> element, and the dir="" of the <bdo> gets flipped.)

* what happens when the selection spans blocks, floats, positioned elements, scrolling containers?

* what happens when the selection spans from SVG to HTML, where the SVG has a reflection transform?

* combinations of the above.
Comment 13 Aryeh Gregor 2011-01-13 00:56:28 UTC
I added some more tests, but as discussed on IRC, I don't think it makes sense to do loads of manual tests here.  People just aren't going to run them enough to make it worth it to test comprehensively, given the low odds of any given test finding additional bugs.  I'll do zillions of automated tests when I spec extend().